/*
 * Copyright 2021 Shaoguang.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "TinyScreenRecorder.h"

#include <chrono>
#include <atomic>

#ifdef __cplusplus
extern "C" {
#endif
// source from luvcview (https://github.com/ksv1986/luvcview)
#include "3rdparty/avilib.h"
#ifdef __cplusplus
} // extern "C"
#endif

#include <QThread>
#include <QApplication> // qmake: QT += widgets
#include <QWidget>
#include <QDesktopWidget>
#include <QScreen>
#include <QBuffer>
#include <QDir>
#include <QFile>
#include <QElapsedTimer>
#include <QDebug>

class TinyScreenRecorderPrivate : public QThread
{
public:
    explicit TinyScreenRecorderPrivate(QObject *parent = nullptr);
    ~TinyScreenRecorderPrivate();

    void stop();
    bool waitForFinished(int msec = 30000);

    /**
     * Fixed bug for MSVC compiler on windows (debug mode):
     *   "ASSERT failure in QCoreApplication::sendEvent: "Cannot send events to objects owned by a different thread. "
     *   "Current thread 0x0x15535b5e000. Receiver 'desktopWindow' (of type 'QWidgetWindow') was created in thread 0x0x15535b4e260", "
     *   "file kernel\qcoreapplication.cpp, line 558"
     */
    static inline void tryGetWinId(QWidget* widget);

    TinyScreenRecorder* recorder{ nullptr };
    int pixmapQuality{ 75 };
    bool autoFps{ true };
    double fps{ 10 };
    char aviCompressor[5]{ "MJPG" };
    QString outputFileName;

    QWidget* targetWidget{ nullptr };
    QRect roiRect;

    std::atomic_bool m_loopFlag{ false };
    std::atomic_bool running{ false };

    static bool aviErrorPrintable;

protected:
    void run() override;

    void tryPrintAviError();
    double detectFPS();

    void emitErrorString(const QString& error);
    void emitRecordingStarted();
    void emitFrameRecorded(qint64 frameCount);
    void emitFrameMissed(qint64 frameCount);
    void emitRecordingFinished();
    void emitFpsDetected(double fps);

    template<class UnaryPredicate>
    bool startRecording(const QString& fileName, bool invokeEvent, UnaryPredicate pred);
};

bool TinyScreenRecorderPrivate::aviErrorPrintable = true;

/*****************************************************************************
  TinyScreenRecorderPrivate member functions
 *****************************************************************************/

/**
 * Fixed bug for MSVC compiler on windows (debug mode):
 *   "ASSERT failure in QCoreApplication::sendEvent: "Cannot send events to objects owned by a different thread. "
 *   "Current thread 0x0x15535b5e000. Receiver 'desktopWindow' (of type 'QWidgetWindow') was created in thread 0x0x15535b4e260", "
 *   "file kernel\qcoreapplication.cpp, line 558"
 */
void TinyScreenRecorderPrivate::tryGetWinId(QWidget* widget)
{
#if defined(Q_CC_MSVC)
    auto wid = widget->winId();
    Q_UNUSED(wid);
#endif
}

TinyScreenRecorderPrivate::TinyScreenRecorderPrivate(QObject *parent) : QThread(parent)
{
    targetWidget = QApplication::desktop();
    tryGetWinId(targetWidget);
}

TinyScreenRecorderPrivate::~TinyScreenRecorderPrivate()
{
    stop();
    this->exit();
}

void TinyScreenRecorderPrivate::stop()
{
    m_loopFlag = false;
}

bool TinyScreenRecorderPrivate::waitForFinished(int msec)
{
    if (!this->running) {
        return true;
    }

    QElapsedTimer elapsedTimer;
    elapsedTimer.start();
    while (elapsedTimer.elapsed() < static_cast<qint64>(msec)) {
        if (!this->running) {
            return true;
        }
    }
    return false;
}

double TinyScreenRecorderPrivate::detectFPS()
{
    double fps = -1.0;

    QString tmpVideoFileName = QDir::tempPath() + "/.TinyScreenRecorderTemp.avi";

    using chrono_clock_type = std::chrono::high_resolution_clock;
    using chrono_seconds_t = std::chrono::duration<double, std::chrono::seconds::period>;
    using chrono_time_point_t = typename std::chrono::high_resolution_clock::time_point;

    chrono_time_point_t timePoint;
    int frameIndex = 0;
    const int frameCount = 5;
    auto lmd_recordingRoutine = [&timePoint, &frameIndex, frameCount]() {
        if (frameIndex == 0) {
            timePoint = std::chrono::high_resolution_clock::now();
        }
        frameIndex++;
        return frameIndex <= frameCount;
    };

    if (startRecording(tmpVideoFileName, false, lmd_recordingRoutine)) {
        double seconds = std::chrono::duration_cast<chrono_seconds_t>(chrono_clock_type::now() - timePoint).count();
        fps = 1.0 / (seconds / static_cast<double>(frameCount));
        fps = static_cast<int>(fps);
    }

    QFile::remove(tmpVideoFileName);

    return fps;
}

void TinyScreenRecorderPrivate::run()
{
    if (this->autoFps) {
        this->fps = detectFPS();
        emitFpsDetected(this->fps);
        qDebug() << fps;
    }

    if (this->fps < 1.0) {
        this->fps = 1.0;
    }

    this->running = true;
    emitRecordingStarted();
    startRecording(this->outputFileName, true, [](){return true;});
    this->running = false;
    emitRecordingFinished();
}

template<class UnaryPredicate>
bool TinyScreenRecorderPrivate::startRecording(const QString& fileName, bool invokeEvent, UnaryPredicate pred)
{
    avi_t* aviHandle = AVI_open_output_file(fileName.toLocal8Bit().data());
    if (!aviHandle) {
        emitErrorString(QString("open output file \"%1\" failed").arg(fileName));
        tryPrintAviError();
        return false;
    }

    if (!this->roiRect.isValid()) {
        this->roiRect = this->targetWidget->geometry();
    }

    AVI_set_video(aviHandle, this->targetWidget->width(), this->targetWidget->height(), this->fps, this->aviCompressor);

    qint64 frameCount = 0;
    qint64 frameMissed = 0;
    m_loopFlag = true;
    while (pred() && m_loopFlag) {
        QPixmap pixmap = QApplication::primaryScreen()->grabWindow(
                             this->targetWidget->winId(),
                             this->roiRect.x(), this->roiRect.y(),
                             this->roiRect.width(), this->roiRect.height());
        QByteArray data;
        QBuffer buff(&data);
        if (!pixmap.save(&buff, "jpg", this->pixmapQuality)) {
            emitErrorString(QString("save screenshot pixmap failed"));
            continue;
        }

        if (0 != AVI_write_frame(aviHandle, buff.buffer().data(), buff.size(), true)) {
            emitErrorString(QString("missing frame"));
            tryPrintAviError();
            if (invokeEvent) {
                emitFrameMissed(++frameMissed);
            }
        }
        else {
            if (invokeEvent) {
                emitFrameRecorded(++frameCount);
            }
        }
    }

    if (0 != AVI_close(aviHandle)) {
        emitErrorString(QString("close output file \"%1\" failed").arg(fileName));
        tryPrintAviError();
        m_loopFlag = false;
        return false;
    }
    m_loopFlag = false;
    return true;
}

void TinyScreenRecorderPrivate::emitErrorString(const QString& error)
{
    if (!QMetaObject::invokeMethod(recorder, "errorOccurred", Q_ARG(QString, error))) {
        qWarning("invoke errorOccurred failed");
    }
}

void TinyScreenRecorderPrivate::emitRecordingStarted()
{
    if (!QMetaObject::invokeMethod(recorder, "recordingStarted")) {
        qWarning("invoke recordingStarted failed");
    }
}

void TinyScreenRecorderPrivate::emitFrameRecorded(qint64 frameCount)
{
    if (!QMetaObject::invokeMethod(recorder, "frameRecorded", Q_ARG(qint64, frameCount))) {
        qWarning("invoke frameRecorded failed");
    }
}

void TinyScreenRecorderPrivate::emitFrameMissed(qint64 frameCount)
{
    if (!QMetaObject::invokeMethod(recorder, "frameMissed", Q_ARG(qint64, frameCount))) {
        qWarning("invoke frameMissed failed");
    }
}

void TinyScreenRecorderPrivate::emitRecordingFinished()
{
    if (!QMetaObject::invokeMethod(recorder, "recordingFinished")) {
        qWarning("invoke recordingFinished failed");
    }
}

void TinyScreenRecorderPrivate::emitFpsDetected(double fps)
{
    if (!QMetaObject::invokeMethod(recorder, "fpsDetected", Q_ARG(double, fps))) {
        qWarning("invoke fpsDetected failed");
    }
}

void TinyScreenRecorderPrivate::tryPrintAviError()
{
    if (TinyScreenRecorderPrivate::aviErrorPrintable) {
        char preffix[] = "[AVILIB]";
        AVI_print_error(preffix);
    }
}

/*****************************************************************************
  TinyScreenRecorder member functions
 *****************************************************************************/

TinyScreenRecorder::TinyScreenRecorder(QObject* parent)
    : QObject(parent)
    , d_ptr(new TinyScreenRecorderPrivate)
{
    Q_D(TinyScreenRecorder);
    d->recorder = this;
}

TinyScreenRecorder::~TinyScreenRecorder()
{
    if (d_ptr) {
        delete d_ptr;
    }
}

void TinyScreenRecorder::setRoiArguments(QWidget* targetWidget, QRect roiRect)
{
    Q_D(TinyScreenRecorder);
    d->targetWidget = targetWidget;
    d->roiRect = roiRect;
    d->tryGetWinId(targetWidget);
}

QWidget* TinyScreenRecorder::targetWidget() const
{
    Q_D(const TinyScreenRecorder);
    return d->targetWidget;
}

QRect TinyScreenRecorder::roiRect() const
{
    Q_D(const TinyScreenRecorder);
    return d->roiRect;
}

void TinyScreenRecorder::start()
{
    Q_D(TinyScreenRecorder);
    d->start();
}

void TinyScreenRecorder::stop()
{
    Q_D(TinyScreenRecorder);
    d->stop();
}

bool TinyScreenRecorder::waitForFinished(int msec)
{
    Q_D(TinyScreenRecorder);
    return d->waitForFinished(msec);
}

bool TinyScreenRecorder::isFinished() const
{
    Q_D(const TinyScreenRecorder);
    return !d->running;
}

bool TinyScreenRecorder::isRunning() const
{
    Q_D(const TinyScreenRecorder);
    return d->running;
}

const QString& TinyScreenRecorder::videoFileName() const
{
    Q_D(const TinyScreenRecorder);
    return d->outputFileName;
}

void TinyScreenRecorder::setVideoFileName(const QString& fileName)
{
    Q_D(TinyScreenRecorder);
    d->outputFileName = fileName;
}

void TinyScreenRecorder::setVideoQuality(int quality)
{
    Q_D(TinyScreenRecorder);
    d->pixmapQuality = quality;
}

int TinyScreenRecorder::videoQuality() const
{
    Q_D(const TinyScreenRecorder);
    return d->pixmapQuality;
}

void TinyScreenRecorder::setVideoFps(double fps)
{
    Q_D(TinyScreenRecorder);
    d->fps = fps;
}

double TinyScreenRecorder::videoFps() const
{
    Q_D(const TinyScreenRecorder);
    return d->fps;
}

void TinyScreenRecorder::setAutoFps(bool automatic)
{
    Q_D(TinyScreenRecorder);
    d->autoFps = automatic;
}

bool TinyScreenRecorder::isAutoFps() const
{
    Q_D(const TinyScreenRecorder);
    return d->autoFps;
}

void TinyScreenRecorder::enablePrintAviError()
{
    TinyScreenRecorderPrivate::aviErrorPrintable = true;
}

void TinyScreenRecorder::disablePrintAviError()
{
    TinyScreenRecorderPrivate::aviErrorPrintable = false;
}

void TinyScreenRecorder::setAviErrorPrintable(bool printable)
{
    TinyScreenRecorderPrivate::aviErrorPrintable = printable;
}

bool TinyScreenRecorder::isAviErrorPrintable()
{
    return TinyScreenRecorderPrivate::aviErrorPrintable;
}

#ifndef QT_NO_DEBUG_STREAM

QDebug operator<<(QDebug debug, const TinyScreenRecorder* recorder)
{
    const QDebugStateSaver saver(debug);
    debug.nospace();
    if (recorder) {
        debug << recorder->metaObject()->className() << "(" << (const void*)recorder << ",\n";

        if (!recorder->objectName().isEmpty()) {
            debug <<"  name = " << recorder->objectName() << ",\n";
        }

        if (recorder->targetWidget()) {
            debug << "  targetWidget = " << recorder->targetWidget() << ",\n";
        }

        debug << "  roiRect = " << recorder->roiRect() << ",\n";
        debug << "  videoFileName = " << recorder->videoFileName() << ",\n";
        debug << "  videoQuality = " << recorder->videoQuality() << ",\n";
        debug << "  videoFps = " << recorder->videoFps() << ",\n";
        debug << "  autoFps = " << recorder->isAutoFps() << ",\n";
        debug << "  isAviErrorPrintable = " << recorder->isAviErrorPrintable();

        debug << "\n)";
    } else {
        debug << "TinyScreenRecorder(0x0)";
    }

    return debug;
}

QDebug operator<<(QDebug debug, const TinyScreenRecorder& recorder)
{
    return (debug << &recorder);
}

#endif
